using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.Contracts;
using System.Linq.Expressions;
using System.Linq;

namespace Charisma.MongoDb.Linq.Expressions
{
    internal sealed class QueryExpression : MongoExpression
    {
        private static readonly ReadOnlyCollection<FieldExpression> emptyFields = new ReadOnlyCollection<FieldExpression>(new List<FieldExpression>());
        private static readonly ReadOnlyCollection<OrderExpression> emptyOrders = new ReadOnlyCollection<OrderExpression>(new List<OrderExpression>());
        
        public QueryExpression(CollectionExpression source)
            : base(MongoExpressionType.Query, source.Type)
        {
            Contract.Requires(source != null);

            Source = source;
            Filter = null;
            GroupBy = null;
            Fields = emptyFields;
            OrderBy = emptyOrders;
            Take = null;
            Skip = null;
            IsDistinct = false;
        }

        private QueryExpression(QueryExpression source)
            : base(MongoExpressionType.Query, source.Type)
        {
            Contract.Requires(source != null);

            Source = source.Source;
            Filter = source.Filter;
            GroupBy = source.GroupBy;
            Fields = source.Fields;
            OrderBy = source.OrderBy;
            Take = source.Take;
            Skip = source.Skip;
            IsDistinct = source.IsDistinct;
        }

        public bool IsDistinct { get; private set; }
        public Expression Source { get; private set; }
        public Expression Filter { get; private set; }
        public Expression GroupBy { get; private set; }
        public ReadOnlyCollection<FieldExpression> Fields { get; private set; }
        public ReadOnlyCollection<OrderExpression> OrderBy { get; private set; }
        public int? Take { get; private set; }
        public int? Skip { get; private set; }

        public QueryExpression SetSkip(int count)
        {
            Contract.Requires(count >= 0);

            if (count == 0)
                return this;

            var skip = Skip == null
                ? count
                : Skip.Value + count;
            var take = Take == null
                ? Take
                : Take.Value - count;

            if (take < 0)
                throw new InvalidOperationException();

            return new QueryExpression(this)
                {
                    Skip = skip,
                    Take = take
                };
        }

        public QueryExpression SetTake(int count)
        {
            Contract.Requires(count >= 0);

            if (count == 0)
                return this;

            return new QueryExpression(this)
                {
                    Take = Take == null
                        ? count
                        : Math.Min(Take.Value, count)
                };
        }

        public QueryExpression SetFilter(Expression filter)
        {
            Contract.Requires(filter != null);

            if (Take != null || Skip != null)
                throw new NotSupportedException();
            return new QueryExpression(this)
                {
                     Filter = Filter == null
                        ? filter
                        : Expression.AndAlso(Filter, filter)
                };
        }

        public QueryExpression SetOrder(IEnumerable<OrderExpression> orders)
        {
            Contract.Requires(orders != null);

            return new QueryExpression(this)
            {
                OrderBy = orders
                    .ToList()
                    .AsReadOnly()
            };
        }

        public QueryExpression AppendOrder(IEnumerable<OrderExpression> orders)
        {
            Contract.Requires(orders!=null);

            return new QueryExpression(this)
                {
                    OrderBy = OrderBy
                        .Concat(orders)
                        .ToList()
                        .AsReadOnly()
                };
        }   

        public QueryExpression SetFields(ReadOnlyCollection<FieldExpression> fields)
        {
            Contract.Requires(fields!=null);

            return new QueryExpression(this)
                {
                    Fields = fields
                };
        }

        public QueryExpression SetDistinct()
        {
            return new QueryExpression(this)
                {
                    IsDistinct = true
                };
        }
    }
}